# oneIP
# oneIP is a library that allows a developer to easily work with IP addresses
# By: Nathan Adams
# email: adamsna@datanethost.net
# phone: (708) 966-9163
# License: Apache License 2.0

# Please see documentation.txt for more info

import operator

class InvalidIPFormat(Exception):
    def __init__(self, value):
        self.val = value
    def __str__(self):
        return "The input " + str(self.val) + " was not a valid IP address in the proper format"

class InvalidIP(Exception):
    def __init__(self, value, type):
        self.val = value
        self.type = type
    def __str__(self):
        return "The input " + str(self.val) + " was not a valid IPv" + str(self.type) + " address in the proper format"

class InvalidIPInput(Exception):
    def __init__(self, value, einput):
        self.val = value
        self.einput = einput
    def __str__(self):
        return "The input " + str(self.val) + " was not an expected object - expected " + str(einput)

class InvalidMask(Exception):
    def __init__(self):
        pass
    def __str__(self):
        return "The subnet mask has not been set"

class IPNotSet(Exception):
    def __init__(self):
        pass
    def __str__(self):
        return "The IP address has not been set"


class IP:
    __cidr = None
    __ipaddr = None
    __type = None
    def __init__(self, ipaddr):
        if self.setIPv4(ipaddr):
            return
        elif self.setIPv6(ipaddr):
            return
        else:
            raise InvalidIPFormat(ipaddr)
            #print "

    def setIPv4(self, ip):
        # IP format is in CIDR notiation - 192.168.128.0/16

        iparr = []
        ipcidrarr = []
        cidr = None
        if ":" in ip:
            return False
        if type(ip) != type(str()):
            raise InvalidIPInput(ip, "string")
        if "/" in ip:
            ipcidrarr = ip.split("/")
            if not self.isValidIPv4(ipcidrarr[0]):
                raise InvalidIP(ip, 4)
            iparr = ipcidrarr[0].split(".")
            self.__cidr = int(ipcidrarr[1])
        else:
            if not self.isValidIPv4(ip):
                raise InvalidIP(ip, 4)
            iparr = ip.split(".")            
        if len(iparr) != 4:
            raise Exception("Not enough octects")
        
        else:
            self.__ipaddr = self._IPv4ToDec(iparr)
            self.__type = 4
        return True

    def isValidIPv4(self, ip):
        try:
            iparr = ip.split(".")
            oct1 = int(iparr[0])
            oct2 = int(iparr[1])
            oct3 = int(iparr[2])
            oct4 = int(iparr[3])
        except:
            raise Excpetion("There was a problem converting the octects to ints")
        if oct1 <= 0 and oct1 >= 255:
            return False
        if oct2 <= 0 and oct2 >= 255:
            return False
        if oct3 <= 0 and oct3 >= 255:
            return False
        if oct4 <= 0 and oct4 >= 255:
            return False
        return True
            
    def isValidIPv6(self, ip):
        try:
            iparr = ip.split(":")
            oct1 = self._hexToBin(iparr[0])
            oct2 = self._hexToBin(iparr[1])
            oct3 = self._hexToBin(iparr[2])
            oct4 = self._hexToBin(iparr[3])
            oct5 = self._hexToBin(iparr[4])
            oct6 = self._hexToBin(iparr[5])
            oct7 = self._hexToBin(iparr[6])
            oct8 = self._hexToBin(iparr[7])
        except:
            raise Exception("There was a problem converting the octects to ints")
        if oct1 <= 0 and oct1 >= 0xFFFF:
            return False
        if oct2 <= 0 and oct2 >= 0xFFFF:
            return False
        if oct3 <= 0 and oct3 >= 0xFFFF:
            return False
        if oct4 <= 0 and oct4 >= 0xFFFF:
            return False
        if oct5 <= 0 and oct5 >= 0xFFFF:
            return False
        if oct6 <= 0 and oct6 >= 0xFFFF:
            return False
        if oct7 <= 0 and oct7 >= 0xFFFF:
            return False
        if oct8 <= 0 and oct8 >= 0xFFFF:
            return False
        return True

    def setIPv6(self, ip):
        # to do
        if "/" in ip:
            ipv6 = ip.split("/")
            self.__cidr = self._getIPv6SubnetMask(int(ipv6[1]))
            ipv6arr = ipv6[0].split(":")
        else:
            ipv6arr = ip.split(":")
        numofsections = None
        if len(ipv6arr) < 8:
            if "::" not in ip:
                return False
        #if "::" in ip:
        #    ipv6arr = self._fillIPv6Address(ipv6arr)
        newipaddr = self._fillIPv6Address(":".join(ipv6arr))
        if not self.isValidIPv6(":".join(newipaddr)):
            raise InvalidIP(ip, 6)
        self.__ipaddr = self._IPv6ToDec(newipaddr)
        self.__type = 6
        return True

    def _fillIPv6Address(self, ipstr):
        sections = []
        tmpip = ""
        if "::" in ipstr:
            ipv6arr = ipstr.split(":")
            numofsections = 8 - len([x for x in ipv6arr if x])
            for i in range(0, numofsections):
                sections.append("0000")
            tmpip = ipstr.replace("::", ":" + ":".join(sections) + ":")
            return tmpip.strip(":").split(":")
        else:
            return ipstr.split(":")

    def _getIPv6SubnetMask(self, cidr):
        # So there really isn't a subnet mask persay but I want to be consistant
        networkmask = ""
        for i in range(0, cidr):
            networkmask += "1"
        for i in range(0, 128 - cidr):
            networkmask += "0"
        return int(networkmask, 2)

    def _getIPv4SubnetMask(self):
        bitstoset = []
        strmask = None
        if self.__cidr == None:
            raise InvalidMask
        else:
            for i in range(0, self.__cidr):
                bitstoset.append(1)
            if len(bitstoset) < 32:
                for i in range(0, 32-len(bitstoset)):
                    bitstoset.append(0)
        strmask = long("".join([str(i) for i in bitstoset]), 2)
        return strmask

    def getIPv4SubnetMask(self):
        return self.toIPv4Notation(self._getIPv4SubnetMask())

    #def getIPv4Range(self):
    #    pass

    def _getNetworkAddress(self):
        return self.__ipaddr & self._getIPv4SubnetMask()

    def _getBroadcastAddress(self):
        return 0xFFFFFFFF & (self.__ipaddr | ~self._getIPv4SubnetMask())

    def _getIPv4FirstAddress(self):
        return (self.__ipaddr & self._getIPv4SubnetMask()) + 1

    def _getIPv4LastAddress(self):
        # Drop to 32bits
        return 0xFFFFFFFF & ((self.__ipaddr | ~self._getIPv4SubnetMask()) -1 )

    def getIPv4NetworkAddress(self):
        return IP.toIPv4Notation(self._getNetworkAddress())

    def getIPv4BroadcastAddress(self):
        return IP.toIPv4Notation(self._getBroadcastAddress())

    def getIPv4FirstAddress(self):
        return IP.toIPv4Notation(self._getIPv4FirstAddress())

    def getIPv4LastAddress(self):
        return IP.toIPv4Notation(self._getIPv4LastAddress())

    def getIPv4Range(self):
        iparr = []
        firstip = self._getIPv4FirstAddress()
        lastip = self._getIPv4LastAddress()
        for ip in range(firstip, lastip+1):
            iparr.append(IP.toIPv4Notation(ip))
        return iparr

    @staticmethod
    def toIPv4Notation(ip):
        return ".".join([str(i) for i in IP.getIPv4Octects(ip)])

    @staticmethod
    def getIPv4Octects(ip):
        octarr = []
        octarr.append(ip & 0xFF)
        octarr.append(( ip >> 8 ) & 0xFF)
        octarr.append(( ip >> 16 ) & 0xFF)
        octarr.append(( ip >> 24 ) & 0xFF)
        octarr.reverse()
        return octarr

    def _decToBin(self, dec):
        if (dec > 0):
            return self._decToBin(dec/2) + str(dec%2)
        return ""

    def _hexToBin(self, hex):
        # http://stackoverflow.com/questions/1425493/convert-hex-to-binary/1425558#1425558
        return bin(int(hex, 16))[2:]

    def _IPv4ToDec(self, iparr):
        oct1 = long(iparr[0])
        oct2 = long(iparr[1])
        oct3 = long(iparr[2])
        oct4 = long(iparr[3])
        iplongstr = self._decToBin(oct1).rjust(8, '0')
        iplongstr += self._decToBin(oct2).rjust(8, '0')
        iplongstr += self._decToBin(oct3).rjust(8, '0')
        iplongstr += self._decToBin(oct4).rjust(8, '0')
        return long(iplongstr, 2)

    def _IPv6ToDec(self, iparr):
        iplongstr = self._hexToBin(iparr[0]).rjust(16, '0')
        iplongstr += self._hexToBin(iparr[1]).rjust(16, '0')
        iplongstr += self._hexToBin(iparr[2]).rjust(16, '0')
        iplongstr += self._hexToBin(iparr[3]).rjust(16, '0')
        iplongstr += self._hexToBin(iparr[4]).rjust(16, '0')
        iplongstr += self._hexToBin(iparr[5]).rjust(16, '0')
        iplongstr += self._hexToBin(iparr[6]).rjust(16, '0')
        iplongstr += self._hexToBin(iparr[7]).rjust(16, '0')
        return int(iplongstr, 2)

    def getOct(self, num):
        if self.__type and self.__type == 4:
            # 8 bit octects
            return (self.__ipaddr >> ( 8 * ( num - 1 ) ) ) & 0xFF
        elif self.__type and self.__type == 6:
            # 16 bit octects
            return (self.__ipaddr >> ( 16 * ( num - 1 ) ) ) & 0xFFFF
        elif not self.__type:
            raise IPNotSet()
        raise IPNotSet()

    def _decToIPv6(self, ip):
        octarr = []
        octarr.append((ip >> 0) & 0xFFFF)
        octarr.append((ip >> 16) & 0xFFFF)
        octarr.append((ip >> 32) & 0xFFFF)
        octarr.append((ip >> 64) & 0xFFFF)
        octarr.append((ip >> 80) & 0xFFFF)
        octarr.append((ip >> 96) & 0xFFFF)
        octarr.append((ip >> 112) & 0xFFFF)
        octarr.reverse()
        return ":".join([hex(n)[2:].strip("L").upper() for n in octarr])

    def getIPv6Range(self):
        #hostmask = ~self.__cidr # this should give us the last address
        firstaddress = self._getIPv6FirstAddress()
        lastaddress = self._getIPv6LastAddress()
        ipv6arr = []
        for ip in self._lrange(firstaddress, lastaddress):
            ipv6arr.append(self._decToIPv6(ip))
        return ipv6arr

    def _getIPv6FirstAddress(self):
        return (self.__ipaddr & self.__cidr)

    def _getIPv6LastAddress(self):
        return 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF & ((self.__ipaddr | ~self.__cidr) )
    
    def getIPv6FirstAddress(self):
        return self._decToIPv6(self._getIPv6FirstAddress())

    def getIPv6LastAddress(self):
        return self._decToIPv6(self._getIPv6LastAddress())

    def isIPv6AddressinRange(self, ip):
        #firsaddress = self.getI
        firstaddress = self._getIPv6FirstAddress()
        lastaddress = self._getIPv6LastAddress()
        address = self._IPv6ToDec(self._fillIPv6Address(ip))
        if firstaddress <= address and lastaddress >= address:
            return True
        else:
            return False

    def isIPv4AddressinRange(self, ip):
        firstaddress = self._getIPv4FirstAddress()
        lastaddress = self._getIPv4LastAddress()
        address = self._IPv4ToDec()
        if firstaddress <= address and lastaddress >= address:
            return True
        else:
            return False

    def _lrange(self, num1, num2 = None, step = 1):
        op = operator.__lt__

        if num2 is None:
            num1, num2 = 0, num1
        if num2 < num1:
            if step > 0:
                num1 = num2
            op = operator.__gt__
        elif step < 0:
            num1 = num2

        while op(num1, num2):
            yield num1
            num1 += step


#ipobj = IP("192.168.128.0/25")
ipobj = IP("192.168.1.0/25")
#ipobj = IP("2620:0:860:2::/64")
print ipobj.getIPv4Range()
print ipobj.getIPv4SubnetMask()
#ipobj.getIPv4Range()
#print ipobj.isIPv6AddressinRange("2621:0:860:2::1")
#print hex(ipobj.getOct(1))
#print IP.toIPNotation(ipobj._IP__ipaddr)
#print IP.toIPNotation(ipobj._getIPv4SubnetMask())
#print IP.toIPNotation(ipobj._getIPv4LastAddress())
#print IP.toIPNotation(ipobj._IP__ipaddr)
#print str(ipobj.getIPv4FirstAddress())
#print ipobj.getIPv4Range()
#print ipobj.getIPv4FirstAddressStr()
#print ipobj.getIPv4LastAddressStr()
#print ipobj.getIPv4Range()